/* Copyright JS Foundation and other contributors, http://js.foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <stdio.h>
#include <stdlib.h>

#include "cli.h"

/*
 * Fixed layout settings
 */

/**
 * Wrap lines at:
 */
#define CLI_LINE_LENGTH 80

/**
 * Indent various lines with:
 */
#define CLI_LINE_INDENT 2

/**
 * Tab stop (for multi-column display) at:
 */
#define CLI_LINE_TAB 24

/**
 * Declare a char VLA and concatenate a program name and a sub-command name
 * (separated by a single space) into the new array. Useful for printing command
 * line option usage summary for sub-commands.
 *
 * @param CMDNAME name of the new array variable.
 * @param PROGNAME string containing the name of the program.
 * @param CMD string continaing the name of the sub-command.
 */
#define CLI_CMD_NAME(CMDNAME, PROGNAME, CMD)            \
  char CMDNAME[strlen((PROGNAME)) + strlen((CMD)) + 2]; \
  strncpy(CMDNAME, (PROGNAME), strlen((PROGNAME)));     \
  CMDNAME[strlen((PROGNAME))] = ' ';                    \
  strncpy(CMDNAME + strlen((PROGNAME)) + 1, (CMD), strlen((CMD)) + 1)

/*
 * Command line option handling
 */

/**
 * Initialize a command line option processor.
 *
 * @return the state that should be passed to other cli_ functions.
 */
cli_state_t cli_init(
    const cli_opt_t* options_p, /**< array of option definitions, terminated by CLI_OPT_DEFAULT */
    int argc,                   /**< number of command line arguments */
    char** argv)                /**< array of command line arguments */
{
  return (cli_state_t){.error = NULL, .arg = NULL, .argc = argc, .argv = argv, .opts = options_p};
} /* cli_init */

/**
 * Use another option list.
 */
void cli_change_opts(
    cli_state_t* state_p,       /**< state of the command line option processor */
    const cli_opt_t* options_p) /**< array of option definitions, terminated by CLI_OPT_DEFAULT */
{
  state_p->opts = options_p;
} /* cli_change_opts */

/**
 * Checks whether the current argument is an option.
 *
 * Note:
 *   The state_p->error is not NULL on error and it contains the error message.
 *
 * @return the ID of the option that was found or a CLI_OPT_ constant otherwise.
 */
int cli_consume_option(cli_state_t* state_p) /**< state of the command line option processor */
{
  if (state_p->error != NULL) {
    return CLI_OPT_END;
  }

  if (state_p->argc <= 0) {
    state_p->arg = NULL;
    return CLI_OPT_END;
  }

  const char* arg = state_p->argv[0];

  state_p->arg = arg;

  if (arg[0] != '-' || arg[1] == '\0') {
    return CLI_OPT_DEFAULT;
  }

  if (arg[1] == '-') {
    arg += 2;

    for (const cli_opt_t* opt = state_p->opts; opt->id != CLI_OPT_DEFAULT; opt++) {
      if (opt->longopt != NULL && strcmp(arg, opt->longopt) == 0) {
        state_p->argc--;
        state_p->argv++;
        return opt->id;
      }
    }

    state_p->error = "Unknown long option";
    return CLI_OPT_END;
  }

  arg++;

  for (const cli_opt_t* opt = state_p->opts; opt->id != CLI_OPT_DEFAULT; opt++) {
    if (opt->opt != NULL && strcmp(arg, opt->opt) == 0) {
      state_p->argc--;
      state_p->argv++;
      return opt->id;
    }
  }

  state_p->error = "Unknown option";
  return CLI_OPT_END;
} /* cli_consume_option */

/**
 * Returns the next argument as string.
 *
 * Note:
 *   The state_p->error is not NULL on error and it contains the error message.
 *
 * @return argument string
 */
const char* cli_consume_string(
    cli_state_t* state_p) /**< state of the command line option processor */
{
  if (state_p->error != NULL) {
    return NULL;
  }

  if (state_p->argc <= 0) {
    state_p->error = "Expected string argument";
    state_p->arg = NULL;
    return NULL;
  }

  state_p->arg = state_p->argv[0];

  state_p->argc--;
  state_p->argv++;
  return state_p->arg;
} /* cli_consume_string */

/**
 * Returns the next argument as integer.
 *
 * Note:
 *   The state_p->error is not NULL on error and it contains the error message.
 *
 * @return argument integer
 */
int cli_consume_int(cli_state_t* state_p) /**< state of the command line option processor */
{
  if (state_p->error != NULL) {
    return 0;
  }

  state_p->error = "Expected integer argument";

  if (state_p->argc <= 0) {
    state_p->arg = NULL;
    return 0;
  }

  state_p->arg = state_p->argv[0];

  char* endptr;
  long int value = strtol(state_p->arg, &endptr, 10);

  if (*endptr != '\0') {
    return 0;
  }

  state_p->error = NULL;
  state_p->argc--;
  state_p->argv++;
  return (int)value;
} /* cli_consume_int */

/*
 * Print helper functions
 */

/**
 * Pad with spaces.
 */
static void cli_print_pad(int cnt) /**< number of spaces to print */
{
  for (int i = 0; i < cnt; i++) {
    printf(" ");
  }
} /* cli_print_pad */

/**
 * Print the prefix of a string.
 */
static void cli_print_prefix(const char* str, /**< string to print */
                             int len)         /**< length of the prefix to print */
{
  for (int i = 0; i < len; i++) {
    printf("%c", *str++);
  }
} /* cli_print_prefix */

/**
 * Print usage summary of options.
 */
static void cli_opt_usage(const char* prog_name_p,    /**< program name, typically argv[0] */
                          const char* command_name_p, /**< command name if available */
                          const cli_opt_t* opts_p)    /**< array of command line option definitions,
                                                         terminated by CLI_OPT_DEFAULT */
{
  int length = (int)strlen(prog_name_p);
  const cli_opt_t* current_opt_p = opts_p;

  printf("%s", prog_name_p);

  if (command_name_p != NULL) {
    int command_length = (int)strlen(command_name_p);

    if (length + 1 + command_length > CLI_LINE_LENGTH) {
      length = CLI_LINE_INDENT - 1;
      printf("\n");
      cli_print_pad(length);
    }

    printf(" %s", command_name_p);
  }

  while (current_opt_p->id != CLI_OPT_DEFAULT) {
    const char* opt_p = current_opt_p->opt;
    int opt_length = 2 + 1;

    if (opt_p == NULL) {
      opt_p = current_opt_p->longopt;
      opt_length++;
    }

    opt_length += (int)strlen(opt_p);

    if (length + 1 + opt_length >= CLI_LINE_LENGTH) {
      length = CLI_LINE_INDENT - 1;
      printf("\n");
      cli_print_pad(length);
    }
    length += opt_length;

    printf(" [");

    if (current_opt_p->opt != NULL) {
      printf("-%s", opt_p);
    } else {
      printf("--%s", opt_p);
    }

    if (current_opt_p->meta != NULL) {
      printf(" %s", current_opt_p->meta);
    }

    printf("]");

    current_opt_p++;
  }

  if (current_opt_p->meta != NULL) {
    const char* opt_p = current_opt_p->meta;
    int opt_length = (int)(2 + strlen(opt_p));

    if (length + 1 + opt_length >= CLI_LINE_LENGTH) {
      length = CLI_LINE_INDENT - 1;
      printf("\n");
      cli_print_pad(length);
    }

    printf(" [%s]", opt_p);
  }

  printf("\n\n");
} /* cli_opt_usage */

/**
 * Print a help message wrapped into the second column.
 */
static void cli_print_help(const char* help) /**< the help message to print */
{
  while (help != NULL && *help != 0) {
    int length = -1;
    int i = 0;
    for (; i < CLI_LINE_LENGTH - CLI_LINE_TAB && help[i] != 0; i++) {
      if (help[i] == ' ') {
        length = i;
      }
    }
    if (length < 0 || i < CLI_LINE_LENGTH - CLI_LINE_TAB) {
      length = i;
    }

    cli_print_prefix(help, length);

    help += length;
    while (*help == ' ') {
      help++;
    }

    if (*help != 0) {
      printf("\n");
      cli_print_pad(CLI_LINE_TAB);
    }
  }
} /* cli_print_help */

/**
 * Print detailed help for options.
 */
void cli_help(
    const char* prog_name_p,    /**< program name, typically argv[0] */
    const char* command_name_p, /**< command name if available */
    const cli_opt_t*
        options_p) /**< array of command line option definitions, terminated by CLI_OPT_DEFAULT */
{
  cli_opt_usage(prog_name_p, command_name_p, options_p);

  const cli_opt_t* opt_p = options_p;

  while (opt_p->id != CLI_OPT_DEFAULT) {
    int length = CLI_LINE_INDENT;
    cli_print_pad(CLI_LINE_INDENT);

    if (opt_p->opt != NULL) {
      printf("-%s", opt_p->opt);
      length += (int)(strlen(opt_p->opt) + 1);
    }

    if (opt_p->opt != NULL && opt_p->longopt != NULL) {
      printf(", ");
      length += 2;
    }

    if (opt_p->longopt != NULL) {
      printf("--%s", opt_p->longopt);
      length += (int)(strlen(opt_p->longopt) + 2);
    }

    if (opt_p->meta != NULL) {
      printf(" %s", opt_p->meta);
      length += 1 + (int)strlen(opt_p->meta);
    }

    if (opt_p->help != NULL) {
      if (length >= CLI_LINE_TAB) {
        printf("\n");
        length = 0;
      }
      cli_print_pad(CLI_LINE_TAB - length);
      length = CLI_LINE_TAB;

      cli_print_help(opt_p->help);
    }

    printf("\n");
    opt_p++;
  }

  if (opt_p->help != NULL) {
    int length = 0;

    if (opt_p->meta != NULL) {
      length = (int)(CLI_LINE_INDENT + strlen(opt_p->meta));

      cli_print_pad(CLI_LINE_INDENT);
      printf("%s", opt_p->meta);
    }

    if (length >= CLI_LINE_TAB) {
      printf("\n");
      length = 0;
    }

    cli_print_pad(CLI_LINE_TAB - length);

    cli_print_help(opt_p->help);
    printf("\n");
  }
} /* cli_help */
